

import express, { ErrorRequestHandler, RequestHandler, Response, json, urlencoded } from 'express'
import { Application } from '@core/types'
import { parseJSON, toNumber } from '@tools/core'
import { v4 } from 'uuid'
import { join } from 'path';
import { loadProto, requestToBuffer } from './proto';
import { publishToTopic } from '@nodejs/mq';
import { listenResponseQueue } from './listenResponseQueue';
import multer, { diskStorage } from 'multer';
import { ensureDirSync, existsSync, readFileSync } from 'fs-extra';
import dayjs from 'dayjs';
import os from 'os'
import { createServer, ServerOptions } from 'https'
import http from 'http'
import { WebSocketServer } from 'ws';
import ssoCode from './ssoCode'
import ssoToken from './ssoToken'
import ssoUser from './ssoUser'
declare global {
    namespace Express {
        interface Request {
            uuid: string;
            requestTime: number;
            uploadsRoot: string;
            sker: { name: string, path: string, version: string }
        }
        interface Response {
            toSuccess: <T>(data: T) => void;
            toFail: <T>(msg: string, data?: T) => void;
        }
    }
}

import upload from './upload'
import uploadImage from './uploadImage'
import { createLogger } from '@nodejs/logger';
import { del, redis, waitFor, zAdd, zRem } from '@nodejs/redis';
import { useSecret } from './utils';
import jwt from 'jsonwebtoken';
import { URL } from 'url'
import { SsoLog, useEntityManager } from './entities';
import { handlerResponseFactory } from './handlerResponse';
import { createRequestRoom, createResponseRoom } from './ws';
export async function expressFactory(appId: string, isMaster: boolean): Promise<Application> {
    const homeDir = os.homedir()
    if (!appId) {
        appId = v4()
    }
    const app = express()
    app.use(require('cookie-parser')())
    let server = http.createServer(app)

    async function createWsServer() {
        const logger = await createLogger()
        const wsServer = new WebSocketServer({ server })
        const handlerResponse = await handlerResponseFactory()
        await del(`ws.online`)
        wsServer.on('close', () => {
            createWsServer()
        });
        const toUrl = (u: string) => {
            if (u.startsWith('http')) {
                return u;
            }
            if (u.startsWith('//')) {
                return `http:${u}`
            }
            if (u.startsWith('/')) {
                return `http://localhost:3000${u}`
            }
            return u;
        }
        // ws 相应 1-1
        const responseWsMap = await createResponseRoom()
        wsServer.on('connection', async (ws, req) => {
            const url = req.url
            let uuid = v4();
            let name = 'main'
            let removeType = async () => { }
            if (url) {
                const _ = new URL(toUrl(url))
                uuid = _.searchParams.get('uuid') || v4()
                if (uuid === 'null') {
                    uuid = v4()
                }
                name = _.searchParams.get('name') || 'main'

                const type = _.searchParams.get('type')
                if (type) {
                    await zAdd(`ws.online.${type}`, Date.now(), uuid)
                    removeType = async () => {
                        await zRem(`ws.online.${type}`, uuid)
                    }
                }
            }
            const ip = (req.headers['x-forwarded-for'] || req.socket.remoteAddress) as string;
            responseWsMap.add(uuid, ws)

            await zAdd(`ws.online`, Date.now(), uuid)
            const removeOnLine = async () => {
                await zRem(`ws.online`, uuid)
            }
            /**
             * 监听返回消息 然后发送
             */
            const pinginterval = 20 * 1000;
            const ponginterval = 20 * 1000;
            let pingInterval: any = null;
            let pongTimeouter: any = null;
            const sendPing = () => {
                ws.send(JSON.stringify({ type: 'PING' }), (err) => {
                    if (err) {
                        return;
                    }
                    pongTimeouter = setTimeout(() => {
                        logger.info('Pong timeout error');
                        ws.terminate()
                    }, 1000 * 10)
                });
            };
            pingInterval = setInterval(sendPing, pinginterval + ponginterval);
            ws.on('pong', () => {
                if (pongTimeouter) clearTimeout(pongTimeouter)
            });
            ws.on('ping', () => ws.pong())
            ws.on('message', async (msg, isBinary) => {
                const version = `v1`
                const str = msg.toString('utf8')
                const payload = parseJSON(str)
                const { type, payload: data } = payload;
                if (type === 'PONG') {
                    if (pongTimeouter) clearTimeout(pongTimeouter)
                    return;
                }
                if (type === 'PING') {
                    ws.send(JSON.stringify({ type: 'PONG' }), (err) => {
                        if (err) {
                            return;
                        }
                    })
                    return;
                }
                if (type === 'TEST') {
                    ws.send(JSON.stringify(data))
                    return;
                }
                if (type === 'REGISTER') {
                    try {
                        const secret = useSecret()
                        const user = jwt.verify(data.token, secret)
                        ws.send(JSON.stringify({ type: 'REGISTER_ACK', payload: user }))
                    } catch (e) {
                        ws.send(JSON.stringify({ type: 'REGISTER_ACK', payload: { success: false, message: (e as any).message } }))
                    }
                    return;
                }
                if (type === 'START_WATCH_REQUEST') {
                    const room = await createRequestRoom(data.name, data.uuid)
                    room.set(data.uuid, ws)
                    ws.send(JSON.stringify({ type: 'START_WATCH_REQUEST_ACK', payload: { success: true } }))
                    return;
                }
                if (type === 'STOP_WATCH_REQUEST') {
                    const room = await createRequestRoom(data.name, data.uuid)
                    room.delete(data.uuid)
                    ws.send(JSON.stringify({ type: 'STOP_WATCH_REQUEST_ACK', payload: { success: true } }))
                    return;
                }
                if (type === 'RESPONSE') {
                    // 这里要做一下转换
                    if (!data.data) {
                        data.data = Buffer.from(JSON.stringify({}))
                    }
                    if (!data.headers) {
                        data.headers = Buffer.from(JSON.stringify({}))
                    }
                    if (!Buffer.isBuffer(data.data)) {
                        if (typeof data.data === 'object') {
                            data.data = Buffer.from(JSON.stringify(data.data))
                        }
                        if (typeof data.headers === 'string') {
                            data.data = Buffer.from(data.data)
                        }
                    }
                    if (!Buffer.isBuffer(data.headers)) {
                        if (typeof data.headers === 'object') {
                            data.headers = Buffer.from(JSON.stringify(data.headers))
                        }
                        if (typeof data.headers === 'string') {
                            data.headers = Buffer.from(data.headers)
                        }
                    }
                    handlerResponse(data, ws)
                    return;
                }
                if (type) {
                    // 自己发 自己回
                    const buf = requestToBuffer(rootProto, {
                        appid: appId,
                        version: version,
                        name: payload.name || name,
                        reqid: uuid,
                        method: `ws`,
                        path: type,
                        ip: ip || '',
                        data: Buffer.from(JSON.stringify({
                            ...(payload.payload || {}),
                            isBinary
                        })),
                        headers: Buffer.from(JSON.stringify(req.headers)),
                    });
                    await publishToTopic(`request.topic`, `request.${payload.name || name}`, buf, {});
                    return;
                } else {
                    ws.send(JSON.stringify({
                        type: 'ERROR',
                        payload: {
                            code: 401,
                            msg: '请求类型错误'
                        }
                    }))
                    return;
                }
            });
            ws.on('close', async () => {
                await removeOnLine();
                if(removeType) await removeType()
                pingInterval && clearInterval(pingInterval)
                pongTimeouter && clearTimeout(pongTimeouter)
            });
            ws.on('error', e => {
                logger.error(e)
            });
            ws.send(JSON.stringify({ type: 'OPEN', payload: { uuid: uuid } }))
        });
        wsServer.on('error', (e) => {
            console.error(e)
        });
    }
    createWsServer()
    if (process.env.HTTPS_KEY && process.env.HTTPS_CERT) {
        const options: ServerOptions = {
            key: readFileSync(join(homeDir, process.env.HTTPS_KEY || 'server.key')),
            cert: readFileSync(join(homeDir, process.env.HTTPS_CERT || 'server.crt'))
        }
        server = createServer(options, app)
    }
    const root = process.cwd()
    app.use((req, res, next) => {
        const origin = req.headers.origin;
        if (origin) {
            res.setHeader('Access-Control-Allow-Origin', origin); // 设置为具体的源
        } else {
            res.setHeader("Access-Control-Allow-Origin", "*");
        }
        res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, PATCH, DELETE");
        res.setHeader("Access-Control-Allow-Headers", "X-Requested-With,Content-Type,Authorization");
        (res as any).setHeader("Access-Control-Allow-Credentials", true);
        // 并且对于预检请求，直接返回200
        if (req.method === 'OPTIONS') {
            res.status(200).end();
        } else {
            next();
        }
    });
    const attachmentRoot = join(homeDir, 'attachments')
    app.use(express.static(attachmentRoot, {}))
    app.use(express.static(join(root, 'public')))
    app.use(json({ limit: '100mb' }))
    app.use(urlencoded({ extended: false }))
    const initHandler: RequestHandler = async (req, res, next) => {
        req.uuid = v4();
        req.requestTime = Date.now();
        req.uploadsRoot = join(homeDir, 'attachments')
        res.toSuccess = async <T>(data: T) => {
            const uuid = req.uuid;
            const requestTime = req.requestTime;
            res.set('uuid', uuid)
            res.set('appid', app.get('uuid'))
            res.set('responseTime', `${Date.now()}`)
            res.set('requestTime', `${requestTime}`)
            res.set('requestInterval', `${Date.now() - requestTime}`)
            res.set('timestamp', `${Date.now()}`)
            if (req.sker) {
                useEntityManager(async m => {
                    const item = await m.findOne(SsoLog, { where: { system: req.sker.name, path: req.sker.path } })
                    if (item) {
                        await m.update(SsoLog, item.id, {
                            total: item.total + 1,
                            avg_time: Math.floor((item.avg_time * item.total + Date.now() - requestTime) / (item.total + 1)),
                            success_count: item.success_count + 1,
                        })
                    } else {
                        const log = m.create(SsoLog, {
                            system: req.sker.name,
                            path: req.sker.path,
                            avg_time: Date.now() - requestTime,
                            total: 1,
                            success_count: 1,
                            fail_count: 0,
                        })
                        await m.save(SsoLog, log)
                    }
                })
            }
            res.json({
                success: true,
                data: data,
                message: 'ok',
                code: 200,
                msg: 'ok'
            })
        }
        res.toFail = async <T>(msg: string, data: T) => {
            const uuid = req.uuid;
            const requestTime = req.requestTime;
            res.set('uuid', uuid)
            res.set('responseTime', `${Date.now()}`)
            res.set('requestTime', `${requestTime}`)
            res.set('requestInterval', `${Date.now() - requestTime}`)
            res.set('timestamp', `${Date.now()}`)
            if (req.sker) {
                useEntityManager(async m => {
                    const item = await m.findOne(SsoLog, { where: { system: req.sker.name, path: req.sker.path } })
                    if (item) {
                        await m.update(SsoLog, item.id, {
                            total: item.total + 1,
                            avg_time: Math.floor((item.avg_time * item.total + Date.now() - requestTime) / (item.total + 1)),
                            fail_count: item.fail_count + 1,
                        })
                    } else {
                        const log = m.create(SsoLog, {
                            system: req.sker.name,
                            path: req.sker.path,
                            avg_time: Date.now() - requestTime,
                            total: 1,
                            success_count: 0,
                            fail_count: 1,
                        })
                        await m.save(SsoLog, log)
                    }
                })
            }
            res.json({
                success: false,
                data: data,
                message: msg,
                code: 200,
                msg: msg
            })
        }
        next()
    }
    app.use(initHandler);
    const storage = diskStorage({
        destination: (req, file, cb) => {
            const datePath = dayjs().format('YYYY/MM/DD')
            ensureDirSync(join(homeDir, 'attachments', datePath))
            cb(null, join(homeDir, 'attachments', datePath))
        },
        filename: (req, file, cb) => {
            const ext = file.originalname.split('.').pop()
            cb(null, `${Date.now()}.${ext}`)
        }
    })
    const uploadMulter = multer({ storage })
    app.post(`/upload`, uploadMulter.single('file'), upload);
    app.post(`/uploadZip`, uploadMulter.single('files'), upload);
    app.post(`/uploads`, uploadMulter.array('files'), upload);
    app.post(`/uploadImage`, uploadMulter.single('file'), uploadImage);
    app.post(`/uploadImages`, uploadMulter.array('files'), uploadImage);
    app.use('/sso/code', ssoCode)
    app.use('/sso/token', ssoToken)
    app.use('/sso/user', ssoUser)
    app.get('/login/qrcode', async (req, res, next) => {
        const reqId = req.query.reqId;
        if (reqId && typeof reqId === 'string') {
            let bufferString: string = await waitFor(reqId)
            while (!bufferString) {
                bufferString = await waitFor(reqId)
                await new Promise((r) => setTimeout(r, 1000))
            }
            const buf = Buffer.from(bufferString, 'base64')
            res.setHeader('Content-Type', 'image/png').send(buf)
            return;
        }
        next(new Error(`reqId is required`))
    })
    app.get('/ws/online', async (req, res, next) => {
        redis(async r => {
            const list = await r.zRange(`ws.online`, 0, -1)
            res.json(list);
        })
    })
    const rootProto = await loadProto()
    const resMap: Map<string, Response> = new Map();
    app.use('/api/:version/:name/:clientId/:path', async (req, res, next) => {
        resMap.set(req.uuid, res)
        const payload: any = {
            ...req.query,
            ...req.body,
            uploadsRoot: req.uploadsRoot
        };
        const name = req.params.name;
        const version = req.params.version;
        const clientId = req.params.clientId;
        const path = req.params.path;
        const buf = requestToBuffer(rootProto, {
            appid: appId,
            version: version,
            name: name,
            reqid: req.uuid,
            method: req.method,
            path: `/${path}`,
            ip: req.ip || '',
            data: Buffer.from(JSON.stringify(payload)),
            headers: Buffer.from(JSON.stringify(req.headers)),
        });
        publishToTopic(`request.topic`, `request.${name}.${clientId}`, buf, {});
    });
    app.use('/api/:version/:name/:path', async (req, res, next) => {
        // 统计分析
        resMap.set(req.uuid, res)
        const payload: any = {
            ...req.query,
            ...req.body,
            uploadsRoot: req.uploadsRoot
        };
        const name = req.params.name;
        const version = req.params.version;
        const path = req.params.path;
        req.sker = {
            name,
            version,
            path
        }
        const buf = requestToBuffer(rootProto, {
            appid: appId,
            version: version,
            name: name,
            reqid: req.uuid,
            method: req.method,
            path: `/${path}`,
            ip: req.ip || '',
            data: Buffer.from(JSON.stringify(payload)),
            headers: Buffer.from(JSON.stringify(req.headers)),
        });
        publishToTopic(`request.topic`, `request.${name}`, buf, {});
    });
    /**
     * 其他业务逻辑
     */
    // 循环查找index.html
    function getIndexHtml(path: string) {
        if (!path) return;
        if (path === '/') return;
        const filePath = join(homeDir, 'attachments', path, 'index.html')
        if (existsSync(filePath)) {
            return filePath;
        }
        path = path.split('/').slice(0, -1).join('/')
        return getIndexHtml(path)
    }
    const notFoundHandler: RequestHandler = (req, res, next) => {
        const filePath = req.path;
        const indexHtml = getIndexHtml(filePath)
        if (indexHtml) {
            res.type('html').sendFile(indexHtml)
            return;
        }
        const uuid = req.uuid;
        const requestTime = req.requestTime;
        res.set('uuid', uuid)
        res.set('responseTime', `${Date.now()}`)
        res.set('requestTime', `${requestTime}`)
        res.set('requestInterval', `${Date.now() - requestTime}`)
        res.set('timestamp', `${Date.now()}`)
        res.status(404).json({
            success: false,
            code: 404,
            error: 'Not Found',
            indexHtml
        })
    }
    app.use(notFoundHandler)
    const errorHandler: ErrorRequestHandler = (error, req, res, next) => {
        const uuid = req.uuid;
        const requestTime = req.requestTime;
        res.set('uuid', uuid)
        res.set('responseTime', `${Date.now()}`)
        res.set('requestTime', `${requestTime}`)
        res.set('requestInterval', `${Date.now() - requestTime}`)
        res.set('timestamp', `${Date.now()}`)
        res.status(500).json({
            success: false,
            code: 500,
            message: error.message,
            timestamp: Date.now(),
            error: error.stack,
        })
    }
    app.use(errorHandler)
    return {
        listen: async () => {
            return new Promise<Function>(async (resolve) => {
                const port = toNumber(process.env.PORT || 3000);
                const host = process.env.HOST || '0.0.0.0';
                const closer = await listenResponseQueue(appId, rootProto, resMap)
                server.listen(port, host)
                resolve(async () => {
                    await closer()
                })
            })
        }
    };
}
